package domain;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import javax.swing.JOptionPane;

/**
 * Responsible for reading voxel.cvs file and populating a Large4DArray object
 * which is made on the concept of memory mapped file to enable the managing 
 * of a number of map sheets of GeoTOP data in one single file. At this class 
 * collects the heading data from the voxel.csv file and turn them into class
 * attributes.
 * @author kooijmanj1
 *
 */
public class Geotop {
	
	// class attributes
	private static final int NRMETALINES = 14;	
	private String name; // name of Random Access File (raf)
	private int aCount; // number of GeoTOP attributes, short for attributeCount
	private Large4DArray array;
	// meta data from csv file, including GeoTOP model attributes
	private String gridType;
	private String sorting;
	private int nx;
	private int ny;
	private int nz;
	private double xllcenter;
	private double yllcenter;
	private double zllcenter;
	private double dx;
	private double dy;
	private double dz;
	private float nodata_value;
	private String[] attributeNames;
	private String[] attributeTypes;

	// constructors
	/**
	 * Constructs Geotop instance from Random Access File and meta data file 
	 * Parameters for files that constitute meta data and content 
	 * for this instance of Geotop
	 * @param meta
	 * @param raf
	 */
	public Geotop(File meta, String rafName){ 
		populateFromRaf(meta, rafName);
	}
	
	/**
	 * Constructs almost empty Geotop instance
	 * Parameter for output files e.g. meta_name.csv and meta_name.raf
	 * @param name
	 */
	public Geotop(String name){ 
		this.name = name; 
	}
	
	/**
	 * Constructs empty Geotop instance
	 */
	public Geotop(){
	}
	
	// getters
	
	/**
	 * @return lineSize
	 */
	public int getLineSize(){
		return aCount;
	}
	public int getXCount(){
		return nx;
	}
	public int getYCount(){
		return ny;
	}
	public int getZCount(){
		return nz;
	}
	public float getNoDataValue(){
		return nodata_value;
	}
	
	/**
	 * Reads first NRMETALINES lines from csv Geotop file through setMetaData method.
	 * Then continues reading from csv Geotop file and populates Large4DArray instance,
	 * that writes the RandomAccessFile.
	 * @param reader
	 * @throws IOException
	 */
	public void populate(BufferedReader reader){
		setMetaDataFromGeotopCsv(reader);
		String line = "";
		String path = "C:/Data/GeoTOP/37wOutput/";
		String RANDOM_ACCESS_FILE_NAME  = path + name + ".raf";
		try {
			array = new Large4DArray(RANDOM_ACCESS_FILE_NAME, aCount,nz,ny,nx);
		}
		catch (IOException e){
			JOptionPane.showMessageDialog(null, "Error creating new raf file");
		}
		float attributeValue = 0f;
		try {
			String[] lineValues = null;
			for (int x = 0; x < nx; x++){
				for (int y = 0; y < ny; y++){
					for (int z = 0; z< nz ; z++){
						line = reader.readLine().trim();
						for (int a = 0 ; a < aCount; a++){
							lineValues = line.split(",");
							attributeValue = Float.parseFloat(lineValues[a]);
							array.set(a, z, y, x, attributeValue);
						}
					}
				}
			}
		}
		catch (IOException e){
			JOptionPane.showMessageDialog(null,"Error reading csv file");
		}
		finally {
			if (reader != null){
				try { reader.close(); }
				catch ( IOException ioe){
					JOptionPane.showMessageDialog(null, "Error closing reader and/or csv file");
				}
			}
		}
	}
	
	/**
	 * Populates instance with data from metadata file and raf.
	 * @param metaData
	 * @param rafFileName
	 */
	public void populateFromRaf(File metaCsv, String rafFileName){
		this.setMetaDataFromMetaCsv(metaCsv);
		try{
			array = new Large4DArray(rafFileName, aCount,nz,ny,nx);
		}
		catch (IOException e){
			JOptionPane.showMessageDialog(null, "Error reading Random Access File");
		}	
	}
	
	/**
	 * Reads a Geotop raf, its meta data file, and a property file.
	 * The property file should contain one additional property 
	 * and its colors. Produces raf enriched with one voxel attribute 
	 * and meta data file adapted for the number of voxel attributes
	 * and attributeNames and attributeTypes extended wit one element.
	 */
	public void enrichRaf(File metaCsvOut, String rafNameOut, LayerProperty litho, 
			LayerProperty prop, String newAttributeName, String newAttributeType){
		//Iterator<Map.Entry<Integer, String>> layerKeys = null;
		int[] layerIndices = litho.getLayerIndices();
		int layerIndex = 0;
		//int layerKey = 0;
		float aValue = 0f;
		float calcAttr = 0f; // calculated attribute
		Large4DArray arrayOut = null;
		//this.setMetaDataFromMetaCsv(metaCsvIn); // metadata al in Geotop aanwezig
		try{
			//array = new Large4DArray(rafNameIn, aCount,nz,ny,nx); // al in Geotop instance aanwezig
			arrayOut = new Large4DArray(rafNameOut, aCount + 1, nz, ny, nx);
		} 
		catch (IOException e){
			JOptionPane.showMessageDialog(null, "Error reading Random Access File");
		}	
		
		for(int x = 0; x < nx; x++){
			for(int y = 0; y < ny; y++){
				for(int z = 0; z < nz; z++){
					//layerKeys = litho.getLayerKeys();
					calcAttr = 0f;
					for(int a = 0; a <aCount; a++){	
						aValue = array.get(a, z, y, x);
						arrayOut.set(a, z, y, x, aValue ); //Voxel attributes of array copied to arrayOut						
						if( (a > 1) && (aValue != nodata_value) ){
							//System.out.println("aValue " + aValue);
							//if(layerKeys.hasNext()){ // condition: as many layer keys as property columns in array
								//layerKey = layerKeys.next().getKey();
								//
								layerIndex = layerIndices[a - 2];
								calcAttr = calcAttr + aValue * LayerProperty.props.get(layerIndex);
								System.out.println("layerIndex regel 199" + layerIndex);
							//}
						}
					}
					arrayOut.set(aCount, z, y, x, calcAttr/2.0f); //extra column toegevoegd met gecalculeerd attribute
				}
			}
		}
		String[] extended = new String[aCount + 1]; // create preliminary array
		extended[aCount] = newAttributeName;
		System.arraycopy(attributeNames, 0, extended, 0, aCount);
		attributeNames = new String[aCount + 1];
		System.arraycopy(extended, 0, attributeNames, 0, aCount + 1); // string with original name extended
		extended = new String[aCount + 1];			// create preliminary array
		extended[aCount] = newAttributeType;
		System.arraycopy(attributeTypes, 0, extended, 0, aCount);
		attributeTypes = new String[aCount + 1];
		System.arraycopy(extended, 0, attributeTypes, 0, aCount + 1); // string with original name extended
		aCount++; // as all work with the original value of aCount has been done, now it can be augmented;
		writeMetaData(metaCsvOut);
	}
	
	/**
	 * Gets attribute value from one point of Geotop.Large4DArray
	 * @param aIndex
	 * @param xIndex
	 * @param yIndex
	 * @param zIndex
	 * @return
	 */
	public float getPointAttribute(int aIndex, int xIndex, int yIndex, int zIndex){
		try{
		checkIndexRanges(aIndex, xIndex, yIndex, zIndex);
		}
		catch (IndexException e){
			JOptionPane.showMessageDialog(null, "Index range error, method getPointAttribute");
		}
		return array.get(aIndex, zIndex, yIndex, xIndex);
	}
	
	/**
	 * Gets all attributes from one point of Geotop.Large4DArray.
	 * @param aCount
	 * @param xIndex
	 * @param yIndex
	 * @param zIndex
	 * @return
	 */
	public float[] getPointAttributes(int aCount, int xIndex, int yIndex, int zIndex){
		try{
			checkIndexRanges(0, xIndex, yIndex, zIndex);
		}
		catch (IndexException e){
			JOptionPane.showMessageDialog(null, "Index range error, method getPointAttributes");
		}
		float[] attributes = new float[aCount];
		for (int index = 0; index < aCount; index++){
			attributes[index] = getPointAttribute(index, zIndex, yIndex, xIndex);
		}
		return attributes;
	}
	
	/**
	 * Returns from selected column the selected attribute.
	 * @param attributeNumber
	 * @param pageNr
	 * @param bookNr
	 * @return
	 */
	public float[] getColumnSelectedAttribute(int aIndex, int xIndex , int yIndex ,  int zCount){
		try{
			checkIndexRanges(aIndex, xIndex, yIndex, 0);
		}
		catch (IndexException e){
			JOptionPane.showMessageDialog(null, "Index range error, method getColumnSelectedAttribute");
		}
		float[] column =  new float[zCount];
		for (int index = 0; index < zCount; index++){
			column[index] = getPointAttribute(aIndex, xIndex, yIndex, index);
		}
		return column;
	}
	
	/**
	 * Returns the selected attribute from selected horizontal line in X direction.
	 * @param aIndex
	 * @param xCount
	 * @param yIndex
	 * @param zIndex
	 * @return
	 */
	public float[] getXLineSelectedAttribute(int aIndex, int xCount, int yIndex, int zIndex){
		try{
		checkIndexRanges(aIndex, 0, yIndex, zIndex);
		}
		catch (IndexException e){
			JOptionPane.showMessageDialog(null, "Index range error, " +
					"method getXLineSelectedAttribute");
		}
		float[] xLine = new float[xCount];
		for(int index = 0; index < xCount; index++){
			xLine[index] = array.get(aIndex, index, yIndex, zIndex);
		}
		return xLine;
	}
	
	/**
	 * Returns the selected attribute from selected horizontal line in Y direction.
	 * @param aIndex
	 * @param xIndex
	 * @param yCount
	 * @param zIndex
	 * @return
	 */
	public float[] getYLineSelectedAttribute(int aIndex, int xIndex, int yCount, int zIndex){
		try{
		checkIndexRanges(aIndex, xIndex, 0, zIndex);
		}
		catch (IndexException e){
			JOptionPane.showMessageDialog(null, "Index range error, " +
					"method getYLineSelectedAttribute");
		}
		float[] yLine = new float[yCount];
		for(int index = 0; index < yCount; index++){
			yLine[index] = array.get(aIndex, xIndex, index, zIndex);
		}
		return yLine;
	}
	
	/**
	 * Returns the selected attribute in vertical plane parallel to x-axes.
	 * @param aIndex
	 * @param xCount
	 * @param yIndex
	 * @param zCount
	 * @return
	 */
	public float[][] getXPlaneSelectedAttribute(int aIndex, int xCount, int yIndex, int zCount){
		try{
			checkIndexRanges(aIndex, 0, yIndex, 0);
		}
		catch (IndexException e){
			JOptionPane.showMessageDialog(null, "Index range error, " +
					"method getXplaneSelectedAttribute");
		}
		float[][] xPlane = new float[xCount][zCount];
		for(int x = 0; x < xCount; x++){
			for(int z = 0; z < zCount; z++){
				xPlane[x][z] = getPointAttribute(aIndex, x, yIndex, z);	
			}
		}
		return xPlane;
	}
	
	/**
	 * Returns the selected attribute in vertical plane parallel to y-axes.
	 * @param aIndex
	 * @param xIndex
	 * @param yCount
	 * @param zCount
	 * @return
	 */
	public float[][] getYPlaneSelectedAttribute(int aIndex, int xIndex, int yCount, int zCount){
		try{
			checkIndexRanges(aIndex, xIndex, 0, 0);
		}
		catch (IndexException e){
			JOptionPane.showMessageDialog(null, "Index range error, " +
					"method getYplaneSelectedAttribute");
		}
		float[][] yPlane = new float[yCount][zCount];
		for(int y = 0; y < yCount; y++){
			for(int z = 0; z < zCount; z++){
				yPlane[y][z] = getPointAttribute(aIndex, xIndex, y, z);	
			}
		}
		return yPlane;
	}
	
	public float[][] getZPlaneSelectedAttribute(int aIndex, int xCount, int yCount, int zIndex){
		try{
			checkIndexRanges(aIndex, 0, 0, zIndex);
		}
		catch (IndexException e){
			JOptionPane.showMessageDialog(null, "Index range error, " +
					"method getZplaneSelectedAttribute");
		}
		float[][] zPlane = new float[xCount][yCount];
		for(int x = 0; x < xCount; x++){
			for(int y = 0; y < yCount; y++){
				zPlane[x][y] = getPointAttribute(aIndex, x, y, zIndex);	
			}
		}
		return zPlane;
	}
	

	/**
	 * Write meta data relevant for raf to meta csv file 
	 */
	public void writeMetaData(File file){
		PrintWriter writer = null;
		try {
			writer = new PrintWriter(
					new BufferedWriter(
						new FileWriter(file, false)));
			writer.println("gridType," + gridType);
			writer.println("sorting," + sorting);
			writer.println("nx," + nx);
			writer.println("ny," + ny);
			writer.println("nz," + nz);
			writer.println("xllcenter," + xllcenter);
			writer.println("yllcenter," + yllcenter);
			writer.println("zllcenter," + zllcenter);
			writer.println("dx," + dx);
			writer.println("dy," + dy);
			writer.println("dz," + dz);
			writer.println("nodata_value," + nodata_value);
			
			StringBuffer meta = new StringBuffer();
			meta.append("attributeNames,");
			for(String geotopAttribute : attributeNames) {
				meta.append(geotopAttribute + ",");
			}
			int lastCommaIndex = meta.lastIndexOf(",");
			meta.replace(lastCommaIndex, lastCommaIndex+1, "");
			writer.println(meta.toString());
			
			meta = new StringBuffer();
			meta.append("attributeTypes,");			
			for(String attributeType : attributeTypes) {
				meta.append(attributeType + ",");
			}
			lastCommaIndex = meta.lastIndexOf(",");
			meta.replace(lastCommaIndex, lastCommaIndex+1, "");
			writer.println(meta.toString());
			writer.println("lineSize," + aCount);
			if(writer.checkError()) {
				System.out.println("Error during writing " + file.getName());
			}
		}
		catch (IOException e) {
			JOptionPane.showMessageDialog(null,"Error writing meta data to file");
		}
		finally {
			if ( writer != null){
				writer.close();
			}
		}
	}
	
	/**
	 * In one operation to read a Geotop csv file the BufferedReader parameter here must 
	 * be the same as in method populate(). This is because the 
	 * reading operation in populate() must continue on the line after the meta data 
	 * lines in the Geotop csv file.
	 * @param reader 
	 * @throws IOException
	 */
	private void setMetaDataFromGeotopCsv(BufferedReader reader){
		String line = ""; // line as read from file
		String[] metaData = new String[NRMETALINES]; // one line in the file is one element of metaData array
		String[] metaDataLine = null; // String array that stores class attribute name and value separately
		// read line and store in metaData line by line
		try {
			for (int lineNumber = 0; line != null && lineNumber < NRMETALINES; lineNumber++){
				line = reader.readLine().trim();
				metaData[lineNumber] = line;
			}
		}
		catch (IOException e){
			JOptionPane.showMessageDialog(null, "Error reading meta data lines from voleg file");
		}
		// convert each metaData element in class attribute name and value
		metaDataLine = metaData[0].split(" ",2);
		gridType = metaDataLine[1];
		metaDataLine = metaData[1].split(" ",2);
		sorting = metaDataLine[1]; // dit moet nog naar een werkbare vorm
		metaDataLine = metaData[2].split(" ",2);
		nx = Integer.parseInt(metaDataLine[1]);
		metaDataLine = metaData[3].split(" ", 2);
		ny = Integer.parseInt(metaDataLine[1]);
		metaDataLine = metaData[4].split(" ", 2);
		nz = Integer.parseInt(metaDataLine[1]);
		metaDataLine = metaData[5].split(" ", 2);
		xllcenter = Double.parseDouble(metaDataLine[1]);
		metaDataLine = metaData[6].split(" ", 2);
		yllcenter = Double.parseDouble(metaDataLine[1]);
		metaDataLine = metaData[7].split(" ", 2);
		zllcenter = Double.parseDouble(metaDataLine[1]);
		metaDataLine = metaData[8].split(" ", 2);
		dx = Double.parseDouble(metaDataLine[1]);
		metaDataLine = metaData[9].split(" ", 2);
		dy = Double.parseDouble(metaDataLine[1]);
		metaDataLine = metaData[10].split(" ", 2);
		dz = Double.parseDouble(metaDataLine[1]);
		metaDataLine = metaData[11].split(" ", 2);
		nodata_value = Float.parseFloat(metaDataLine[1]);
		attributeNames = metaData[12].split(",");
		attributeTypes = metaData[13].split(",");
		aCount = attributeNames.length;
		System.out.println("lineSize = " + aCount);	
	}
	
	/**
	 * Reads meta data from meta csv file and populates class attributes
	 * before the raf is populated.
	 * @param metaFile
	 */
	private void setMetaDataFromMetaCsv(File metaFile){
		String line = "";
		int lineNumber = 0;
		String[] metaData = new String[NRMETALINES+1];// one more because of lineSize
		String[] metaDataLine = null;
		BufferedReader reader = null;
		try{
			reader = new BufferedReader(new FileReader(metaFile));
			line = reader.readLine().trim();
			while(line != null && line != ""){
				metaData[lineNumber] = line;
				line = reader.readLine();
				lineNumber++;
			}
			if (lineNumber != NRMETALINES + 1){
				JOptionPane.showMessageDialog(null,"Number of meta data lines not correctly read");
			}
		}
		catch(IOException e){
			JOptionPane.showMessageDialog(null, "Error reading meta data lines from csv file");
		}
		finally{
			if(reader != null){
				try {
					reader.close();
				}
				catch(IOException ioe){}
			}
		}
		// convert each metaData element in class attribute name and value
				
		metaDataLine = metaData[0].split(",", 2);
		gridType = metaDataLine[1];
		metaDataLine = metaData[1].split(",", 2);
		sorting = metaDataLine[1]; // dit moet nog naar een werkbare vorm
		metaDataLine = metaData[2].split(",", 2);
		nx = Integer.parseInt(metaDataLine[1]);
		metaDataLine = metaData[3].split(",", 2);
		ny = Integer.parseInt(metaDataLine[1]);
		metaDataLine = metaData[4].split(",", 2);
		nz = Integer.parseInt(metaDataLine[1]);
		metaDataLine = metaData[5].split(",", 2);
		xllcenter = Double.parseDouble(metaDataLine[1]);
		metaDataLine = metaData[6].split(",", 2);
		yllcenter = Double.parseDouble(metaDataLine[1]);
		metaDataLine = metaData[7].split(",", 2);
		zllcenter = Double.parseDouble(metaDataLine[1]);
		metaDataLine = metaData[8].split(",", 2);
		dx = Double.parseDouble(metaDataLine[1]);
		metaDataLine = metaData[9].split(",", 2);
		dy = Double.parseDouble(metaDataLine[1]);
		metaDataLine = metaData[10].split(",", 2);
		dz = Double.parseDouble(metaDataLine[1]);
		metaDataLine = metaData[11].split(",", 2);
		nodata_value = Float.parseFloat(metaDataLine[1]);
		metaDataLine =  metaData[12].split(",");
		aCount = metaDataLine.length;
		attributeNames = new String[aCount - 1];
		for(int i = 1; i < aCount; i++){
			attributeNames[i-1] = metaDataLine[i];
		}
		metaDataLine = metaData[13].split(",");
		attributeTypes = new String[aCount- 1];
		for(int i = 1; i < aCount; i++){ 
			attributeTypes[i-1] = metaDataLine[i];
		}
		aCount = attributeNames.length;
		System.out.println("lineSize = " + aCount);	
	}
	
	
	/**
	 * Checks indices for subceeding of exceeding of index ranges
	 * in GeoTOP. When all parameters are needed fill 0. Needs improvement
	 * to also check against ranges that do not start at 0.
	 * @param aIndex
	 * @param xIndex
	 * @param yIndex
	 * @param zIndex
	 * @throws IndexException
	 */
	private void checkIndexRanges(int aIndex, int xIndex, int yIndex, int zIndex) throws IndexException{
		if(aIndex < 0 || aIndex >= aCount) throw new IndexException();
		if(xIndex < 0 || xIndex >= nx ) throw new IndexException();
		if(yIndex < 0 || yIndex >= ny ) throw new IndexException();
		if(zIndex < 0 || zIndex >= nz ) throw new IndexException();
	}
}
